package ch.fusun.baron.core.injection;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.Platform;

/**
 * Utility to re-inject beans via dependency injection
 */
public class ReInjector {

	private static final String SERVICE = "service"; //$NON-NLS-1$
	/**
	 * Extension point ID
	 */
	private static final String INJECTEE_ID = "ch.fusun.baron.injection.service"; //$NON-NLS-1$
	private static final String CONFIGURATION_FILE = Platform
			.getInstanceLocation().getURL().getPath()
			+ "/configuration.properties"; //$NON-NLS-1$
	private static ReInjector instance;
	private final Map<Class<?>, Object> services = new HashMap<Class<?>, Object>();
	private final Map<Class<?>, IConfigurationElement> configElements = new HashMap<Class<?>, IConfigurationElement>();
	private final Properties configuration;

	/**
	 * Constructor
	 */
	public ReInjector() {
		IConfigurationElement[] config = Platform.getExtensionRegistry()
				.getConfigurationElementsFor(INJECTEE_ID);
		try {
			for (IConfigurationElement e : config) {
				try {

					Class<?> c = Platform.getBundle(
							e.getContributor().getName()).loadClass(
							e.getAttribute(SERVICE));
					for (Class<?> iface : c.getInterfaces()) {
						configElements.put(iface, e);
					}
				} catch (ClassNotFoundException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
		} catch (InvalidRegistryObjectException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		configuration = new Properties();
		try {
			File file = new File(CONFIGURATION_FILE);
			if (!file.exists()) {
				file.createNewFile();
			}
			configuration.load(new FileReader(file));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Re-injects the object with the local implementations of the required
	 * beans
	 * 
	 * @param o
	 *            The object to re-inject
	 */
	public void reInject(Object o) {
		for (Method method : o.getClass().getMethods()) {
			if (method.getAnnotation(Inject.class) != null) {
				Class<?> injectionClass = method.getParameterTypes()[0];
				Object bean = getInstanceForClass(injectionClass);
				reInject(bean);
				try {
					method.invoke(o, bean);
				} catch (Exception e) {
					e.printStackTrace();
				}

			}
		}

		for (Field field : getAllFields(o.getClass())) {
			if (field.getAnnotation(Configure.class) != null) {
				String property = configuration.getProperty(createUniqueKey(o,
						field));
				if (property == null) {
					property = field.getAnnotation(Configure.class).value();
					try {
						configuration.setProperty(createUniqueKey(o, field),
								property);
						configuration.store(new FileWriter(new File(
								CONFIGURATION_FILE)), property);
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
				field.setAccessible(true);
				try {
					Class<?> type = field.getType();
					if (type.equals(Integer.class) || type.equals(int.class)) {

						field.setInt(o, Integer.parseInt(property));

					} else if (type.equals(Double.class)
							|| type.equals(double.class)) {
						field.setDouble(o, Double.parseDouble(property));
					} else if (type.equals(Float.class)
							|| type.equals(float.class)) {
						field.setFloat(o, Float.parseFloat(property));
					} else if (type.equals(String.class)) {
						field.set(o, property);
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
			if (field.getAnnotation(Inject.class) != null) {
				Class<?> injectionClass = field.getType();
				Object bean = getInstanceForClass(injectionClass);
				reInject(bean);
				try {
					field.setAccessible(true);
					field.set(o, bean);
				} catch (Exception e) {
					e.printStackTrace();
				}

			}
		}
	}

	private List<Field> getAllFields(Class<?> clazz) {
		List<Field> fields = new ArrayList<Field>(Arrays.asList(clazz
				.getDeclaredFields()));
		Class<?> superClass = clazz.getSuperclass();
		if (superClass != null) {
			fields.addAll(getAllFields(superClass));
		}
		return fields;
	}

	private static String createUniqueKey(Object o, Field field) {
		return o.getClass().getCanonicalName() + "." + field.getName(); //$NON-NLS-1$
	}

	/**
	 * Gets the corresponding instance for the class/interface via extension
	 * point
	 * 
	 * @param injectionClass
	 *            The class/interface
	 * @return The instance of that class/interface
	 */
	@SuppressWarnings("unchecked")
	public <T> T getInstanceForClass(Class<T> injectionClass) {
		/*
		 * TODO: If bean not found, possibly we could try to search in super
		 * classes until we reach Object?
		 */
		Object object = services.get(injectionClass);
		if (object != null) {
			return (T) object;
		}
		IConfigurationElement configElement = configElements
				.get(injectionClass);
		if (configElement != null) {

			try {
				Object bean = Platform
						.getBundle(configElement.getContributor().getName())
						.loadClass(configElement.getAttribute(SERVICE))
						.newInstance();
				services.put(injectionClass, bean);
			} catch (InvalidRegistryObjectException e) {
				e.printStackTrace();
				return null;
			} catch (InstantiationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			Object result = services.get(injectionClass);
			if (result == null) {
				System.err.println("No instance found for: " //$NON-NLS-1$
						+ injectionClass.getCanonicalName());
			}
			return (T) result;
		}
		if (injectionClass.getInterfaces().length > 0) {
			return (T) (getInstanceForClass(injectionClass.getInterfaces()[0]));
		}
		return null;
	}

	/**
	 * Gets the unique instance of the {@link ReInjector}
	 * 
	 * @return The singleton
	 */
	public static ReInjector getInstance() {
		if (ReInjector.instance == null) {
			instance = new ReInjector();
		}
		return instance;
	}

	/**
	 * @param clazz
	 *            Updates the instance for the class
	 * @param o
	 *            The new instance
	 */
	public void updateInstance(Class<?> clazz, Object o) {
		for (Class<?> iface : clazz.getInterfaces()) {
			services.put(iface, o);
		}
	}

}
